import { getAPIResponse as getAPIResponseViaExtension } from "actions/ExtensionActions";
import { getAPIResponse as getAPIResponseViaProxy } from "actions/DesktopActions";
import { AbortReason, FormDropDownOptions, KeyValuePair, RQAPI, RequestContentType, RequestMethod } from "../../types";
import { CONSTANTS } from "@requestly/requestly-core";
import {
  CONTENT_TYPE_HEADER,
  DEMO_HTTP_API_URL,
  LARGE_FILE_SIZE,
  SESSION_STORAGE_EXPANDED_RECORD_IDS_KEY,
  DEFAULT_REQUEST_NAME,
} from "../../constants";
import * as curlconverter from "curlconverter";
import { forEach, omit, split } from "lodash";
import { sessionStorage } from "utils/sessionStorage";
import { Request as HarRequest } from "har-format";
import { getDefaultAuth } from "./components/views/components/request/components/AuthorizationView/defaults";
import { ApiClientRecordsInterface } from "features/apiClient/helpers/modules/sync/interfaces";
import { UserAbortError } from "features/apiClient/errors/UserAbortError/UserAbortError";
import { Authorization } from "./components/views/components/request/components/AuthorizationView/types/AuthConfig";
import { inheritAuthFromParent } from "features/apiClient/helpers/auth";
import { AutogeneratedFieldsStore, parseAuth, parseContentType } from "features/apiClient/store/autogenerateStore";
import Papa from "papaparse";
import Ajv, { SchemaObject } from "ajv";
import { utilityWorker } from "features/apiClient/helpers/utilityWorker";
import Logger from "lib/logger";
import { getFileContents } from "components/mode-specific/desktop/DesktopFilePicker/desktopFileAccessActions";
import { NativeError } from "errors/NativeError";
import { trackCollectionRunnerRecordLimitExceeded } from "modules/analytics/events/features/apiClient";
import { getBoundary, parse as multipartParser } from "parse-multipart-data";

const createAbortError = (signal: AbortSignal) => {
  if (signal && signal.reason === AbortReason.USER_CANCELLED) {
    return new UserAbortError();
  }
  return new Error("Request aborted");
};

type ResponseOrError = RQAPI.HttpResponse | { error: string };
export const makeRequest = async (
  appMode: string,
  request: RQAPI.HttpRequest,
  signal?: AbortSignal
): Promise<RQAPI.HttpResponse> => {
  return new Promise((resolve, reject) => {
    const abortListener = () => {
      signal?.removeEventListener("abort", abortListener);
      reject(createAbortError(signal));
    };

    if (signal) {
      if (signal.aborted) {
        return reject(createAbortError(signal));
      }
      signal.addEventListener("abort", abortListener);
    }

    //TODO: make the default value false if and when the feature flag is turned on
    request.includeCredentials = request.includeCredentials ?? true; // Always include credentials for API requests

    if (appMode === CONSTANTS.APP_MODES.EXTENSION) {
      getAPIResponseViaExtension(request)
        .then((result: ResponseOrError) => {
          if (!result) {
            //Backward compatibility check
            reject(new Error("Failed to make request. Please check if the URL is valid."));
          } else if ("error" in result) {
            reject(new Error(result.error));
          } else {
            resolve(result);
          }
        })
        .finally(() => {
          signal?.removeEventListener("abort", abortListener);
        });
    } else if (appMode === CONSTANTS.APP_MODES.DESKTOP) {
      getAPIResponseViaProxy(request)
        .then((result: ResponseOrError) => {
          if (!result) {
            //Backward compatibility check
            reject(new Error("Failed to make request. Please check if the URL is valid."));
          } else if ("error" in result) {
            reject(new Error(result.error));
          } else {
            resolve(result);
          }
        })
        .finally(() => {
          signal?.removeEventListener("abort", abortListener);
        });
    } else {
      signal?.removeEventListener("abort", abortListener);
      resolve(null);
    }
  });
};

// TODO: move this into top level common folder
export const addUrlSchemeIfMissing = (url: string): string => {
  if (url && !/^([a-z][a-z0-9+\-.]*):\/\//.test(url)) {
    return "http://" + url;
  }

  return url;
};

export const getEmptyHttpEntry = (request?: RQAPI.Request): RQAPI.HttpApiEntry => {
  return {
    type: RQAPI.ApiEntryType.HTTP,
    request: {
      url: DEMO_HTTP_API_URL,
      method: RequestMethod.GET,
      headers: [],
      queryParams: [],
      body: null,
      contentType: RequestContentType.RAW,
      ...(request || {}),
    },
    response: null,
    scripts: {
      preRequest: "",
      postResponse: "",
    },
    auth: getDefaultAuth(false),
  };
};

export const getEmptyGraphQLEntry = (request?: RQAPI.Request): RQAPI.GraphQLApiEntry => {
  return {
    type: RQAPI.ApiEntryType.GRAPHQL,
    request: {
      url: "",
      headers: [],
      operation: "",
      variables: "",
      operationName: "",
      ...(request || {}),
    },
    response: null,
    scripts: {
      preRequest: "",
      postResponse: "",
    },
    auth: getDefaultAuth(false),
  };
};

export const getEmptyApiEntry = (apiEntryType: RQAPI.ApiEntryType, request?: RQAPI.Request) => {
  switch (apiEntryType) {
    case RQAPI.ApiEntryType.HTTP:
      return getEmptyHttpEntry(request);
    case RQAPI.ApiEntryType.GRAPHQL:
      return getEmptyGraphQLEntry(request);
    default:
      return getEmptyHttpEntry(request);
  }
};

export const getEmptyDraftApiRecord = (apiEntryType: RQAPI.ApiEntryType, request?: RQAPI.Request): RQAPI.ApiRecord => {
  return {
    data: getEmptyApiEntry(apiEntryType),
    type: RQAPI.RecordType.API,
    id: "",
    name: DEFAULT_REQUEST_NAME,
    collectionId: "",
    ownerId: "",
    deleted: false,
    createdBy: "",
    updatedBy: "",
    createdTs: Date.now(),
    updatedTs: Date.now(),
  };
};

export const sanitizeEntry = (entry: RQAPI.HttpApiEntry, removeInvalidPairs = true) => {
  // Add safety checks for entry and entry.request
  if (!entry || !entry.request) {
    return getEmptyHttpEntry(); // Return a default empty entry if input is invalid
  }

  const sanitizedEntry: RQAPI.HttpApiEntry = {
    ...entry,
    request: {
      ...entry.request,
      queryParams: sanitizeKeyValuePairs(entry.request.queryParams, removeInvalidPairs),
      headers: sanitizeKeyValuePairs(entry.request.headers, removeInvalidPairs),
    },
    scripts: {
      preRequest: entry.scripts?.preRequest || "",
      postResponse: entry.scripts?.postResponse || "",
    },
  };

  if (entry.request.body != null) {
    if (!supportsRequestBody(entry.request.method)) {
      sanitizedEntry.request.body = null;
    } else if (entry.request.contentType === RequestContentType.FORM) {
      sanitizedEntry.request.body = sanitizeKeyValuePairs(
        entry.request.body as RQAPI.RequestFormBody,
        removeInvalidPairs
      );
    } else if (entry.request.contentType === RequestContentType.MULTIPART_FORM) {
      sanitizedEntry.request.body = sanitizeKeyValuePairs(
        entry.request.body as RQAPI.MultipartFormBody,
        removeInvalidPairs
      );
    }
  }

  return sanitizedEntry;
};

/**
 * generic sanitization fx for keyValuePairs (form & multipartform)
 */
export const sanitizeKeyValuePairs = <T extends KeyValuePair>(keyValuePairs: T[], removeInvalidPairs = true): T[] => {
  if (!keyValuePairs) {
    return [];
  }

  // Hotfix for corrupted multipart requests
  const formattedKeyValuePairs = Array.isArray(keyValuePairs)
    ? keyValuePairs
    : (generateKeyValuePairs(keyValuePairs) as T[]);

  return formattedKeyValuePairs
    .map((pair) => ({
      ...pair,
      isEnabled: pair.isEnabled ?? true,
    }))
    .filter((pair) => !removeInvalidPairs || (pair.isEnabled && pair.key?.length > 0));
};

export const supportsRequestBody = (method: RequestMethod): boolean => {
  return ![RequestMethod.GET, RequestMethod.HEAD].includes(method);
};

export const generateKeyValuePairs = (data: string | Record<string, string | string[]> = {}): KeyValuePair[] => {
  const result: KeyValuePair[] = [];
  if (typeof data === "string") {
    data = {
      [data]: "",
    };
  }
  for (const [key, rawValue] of Object.entries(data)) {
    const valueArray = Array.isArray(rawValue) ? rawValue : [rawValue];
    for (const value of valueArray) {
      result.push({
        key: key || "",
        value,
        id: Math.random(),
        isEnabled: true,
      });
    }
  }
  return result;
};

export const getContentTypeFromRequestHeaders = (headers: KeyValuePair[]) => {
  const contentTypeHeader = headers.find((header) => header.key.toLowerCase() === CONTENT_TYPE_HEADER.toLowerCase());
  const contentTypeHeaderValue = contentTypeHeader?.value as RequestContentType;

  const contentType: RequestContentType | undefined =
    contentTypeHeaderValue && Object.values(RequestContentType).find((type) => contentTypeHeaderValue.includes(type));

  return contentType;
};

export const getContentTypeFromResponseHeaders = (headers: KeyValuePair[]): string => {
  return headers.find((header) => header.key.toLowerCase() === CONTENT_TYPE_HEADER.toLowerCase())?.value;
};

export const filterHeadersToImport = (headers: KeyValuePair[]) => {
  return headers.filter((header) => {
    // exclude headers dependent on original source
    if (["host", "accept-encoding"].includes(header.key.toLowerCase())) {
      return false;
    }

    // exclude pseudo headers
    if (header.key.startsWith(":")) {
      return false;
    }

    return true;
  });
};

export const generateMultipartFormKeyValuePairs = (
  data: { key: string; value: string }[]
): RQAPI.FormDataKeyValuePair[] => {
  const result: RQAPI.FormDataKeyValuePair[] = [];

  data.forEach(({ key, value }) => {
    if (typeof value === "string" && value.startsWith("@")) {
      result.push({
        id: Math.random(),
        key: key || "",
        value: [] as RQAPI.MultipartFileValue[],
        isEnabled: true,
        type: FormDropDownOptions.FILE,
      } as RQAPI.FormDataKeyValuePair);
    } else {
      result.push({
        id: Math.random(),
        key: key || "",
        value: value || "",
        isEnabled: true,
        type: FormDropDownOptions.TEXT,
      } as RQAPI.FormDataKeyValuePair);
    }
  });

  return result;
};

export const parseMultipartFormDataString = (
  body: string,
  contentTypeHeader: string | null
): { key: string; value: string; isFile?: boolean; fileName?: string }[] => {
  const result: { key: string; value: string; isFile?: boolean; fileName?: string }[] = [];

  const boundary = (() => {
    if (contentTypeHeader) {
      return getBoundary(contentTypeHeader);
    }

    // Fallback: try to extract boundary from body
    // Extract boundary from the first line
    // In the body, boundaries appear with 2 extra leading dashes (e.g., ------WebKit...)
    // But the actual boundary for parsing should have 2 fewer dashes (e.g., ----WebKit...)
    const boundaryMatch = body.match(/^--(-+\S+)/);
    if (!boundaryMatch) {
      return null;
    }
    return boundaryMatch[1].trim();
  })();

  if (!boundary) {
    return result;
  }

  try {
    const bodyBuffer = Buffer.from(body, "utf-8");

    const parts = multipartParser(bodyBuffer, boundary);

    parts.forEach((part) => {
      const fieldName = part.name;
      if (fieldName) {
        const isFile = !!part.filename;
        result.push({
          key: fieldName,
          value: !isFile ? part.data.toString("utf-8") : "",
          isFile: isFile,
          fileName: part.filename || undefined,
        });
      }
    });
  } catch (error) {
    Logger.log("[parseMultipartFormDataString] Failed to parse multipart data:", error);
  }

  return result;
};

export const parseCurlRequest = (curl: string): RQAPI.Request => {
  const requestJson = curlconverter.toJsonObject(curl);
  const queryParamsFromJson = generateKeyValuePairs(requestJson.queries);
  /*
      cURL converter is not able to parse query params from url for some cURL requests
      so parsing it manually from URL and populating queryParams property
    */
  const requestUrlParams = new URL(requestJson.url).searchParams;
  const paramsFromUrl = generateKeyValuePairs(Object.fromEntries(requestUrlParams.entries()));
  const headersObj = (requestJson.headers ?? {}) as Record<string, string>;
  const headers = filterHeadersToImport(generateKeyValuePairs(headersObj));

  let contentType = getContentTypeFromRequestHeaders(headers);

  // For multipart-form data we need to check the json structure
  const hasFiles = requestJson.files && Object.keys(requestJson.files).length > 0;
  const hasData = requestJson.data && Object.keys(requestJson.data).length > 0;

  if (hasFiles) {
    contentType = RequestContentType.MULTIPART_FORM;
  }

  // Fallback: If content-type is still undefined, check the HTTP string for content-type
  if (!contentType) {
    const httpString = curlconverter.toHTTP(curl);
    if (httpString) {
      const match = httpString.match(/content-type:\s*([^\r\n]+)/i); // stops matching at line break
      if (match) {
        const httpContentType = match[1].trim().toLowerCase();

        if (httpContentType.includes("multipart/form-data")) {
          contentType = RequestContentType.MULTIPART_FORM;
        } else if (httpContentType.includes("application/x-www-form-urlencoded")) {
          contentType = RequestContentType.FORM;
        } else if (httpContentType.includes("application/json")) {
          contentType = RequestContentType.JSON;
        } else {
          contentType = RequestContentType.RAW;
        }
      }
    }
  }

  let body: RQAPI.RequestBody;
  switch (contentType) {
    case RequestContentType.JSON:
      body = JSON.stringify(requestJson.data);
      break;
    case RequestContentType.FORM:
      body = generateKeyValuePairs(requestJson.data);
      break;
    case RequestContentType.MULTIPART_FORM: {
      const multipartData: { key: string; value: string }[] = [];
      if (hasData) {
        for (const [key, value] of Object.entries(requestJson.data)) {
          multipartData.push({ key, value: String(value) });
        }
      }

      if (hasFiles) {
        for (const [key, filePath] of Object.entries(requestJson.files)) {
          multipartData.push({ key, value: `@${filePath}` });
        }
      }
      body = generateMultipartFormKeyValuePairs(multipartData);
      break;
    }
    default:
      body = requestJson.data ?? null; // Body can be undefined which throws an error while saving the request in firestore
      break;
  }

  // remove query params from url
  const requestUrl = requestJson.url.split("?")[0];

  const request: RQAPI.Request = {
    url: requestUrl,
    method: requestJson.method.toUpperCase() as RequestMethod,
    queryParams: queryParamsFromJson.length ? queryParamsFromJson : paramsFromUrl,
    headers,
    contentType,
    body: body ?? null,
    bodyContainer: createBodyContainer({ contentType, body }),
  };

  return request;
};

export const isApiRequest = (record: RQAPI.ApiClientRecord) => {
  return record.type === RQAPI.RecordType.API;
};

export const isApiCollection = (record: RQAPI.ApiClientRecord) => {
  return record?.type === RQAPI.RecordType.COLLECTION;
};

const sortRecords = (records: RQAPI.ApiClientRecord[]) => {
  return records.sort((a, b) => {
    // Sort by type first
    const typeComparison = a.type.localeCompare(b.type);
    if (typeComparison !== 0) return typeComparison;

    // If types are the same, sort alphabetically by name
    return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
  });
};

const sortNestedRecords = (records: RQAPI.ApiClientRecord[]) => {
  records.forEach((record) => {
    if (isApiCollection(record)) {
      record.data.children = sortRecords(record.data.children);
      sortNestedRecords(record.data.children);
    }
  });
};

export const convertFlatRecordsToNestedRecords = (records: RQAPI.ApiClientRecord[]) => {
  const recordsCopy = [...records];
  const recordsMap: Record<string, RQAPI.ApiClientRecord> = {};
  const updatedRecords: RQAPI.ApiClientRecord[] = [];

  recordsCopy.forEach((record) => {
    if (isApiCollection(record)) {
      recordsMap[record.id] = {
        ...record,
        data: { ...record.data, children: [] },
      };
    } else if (isApiRequest(record)) {
      recordsMap[record.id] = record;
    }
  });

  recordsCopy.forEach((record) => {
    const recordState = recordsMap[record.id];
    const parentNode = recordsMap[record.collectionId] as RQAPI.CollectionRecord;
    if (parentNode) {
      parentNode.data.children.push(recordState);
    } else {
      updatedRecords.push({ ...recordState, collectionId: "" });
    }
  });

  sortNestedRecords(updatedRecords);
  return { recordsMap, updatedRecords };
};

export const getEmptyPair = (): KeyValuePair => ({ id: Math.random(), key: "", value: "", isEnabled: true });

export const createBlankApiRecord = (
  recordType: RQAPI.RecordType,
  collectionId: string,
  apiClientRecordsRepository: ApiClientRecordsInterface<any>,
  entryType?: RQAPI.ApiEntryType
) => {
  const newRecord: Partial<RQAPI.ApiClientRecord> = {};

  if (recordType === RQAPI.RecordType.API) {
    newRecord.name = "Untitled request";
    newRecord.type = RQAPI.RecordType.API;
    newRecord.data = getEmptyApiEntry(entryType);
    newRecord.deleted = false;
    newRecord.collectionId = collectionId;
  }

  if (recordType === RQAPI.RecordType.COLLECTION) {
    newRecord.name = "New collection";
    newRecord.type = RQAPI.RecordType.COLLECTION;
    newRecord.data = {
      variables: {},
      auth: getDefaultAuth(false),
    };
    newRecord.deleted = false;
    newRecord.collectionId = collectionId;
  }

  const result =
    recordType === RQAPI.RecordType.COLLECTION
      ? apiClientRecordsRepository.createCollection(newRecord as RQAPI.CollectionRecord)
      : apiClientRecordsRepository.createRecord(newRecord as RQAPI.ApiRecord);

  return result;
};

export const extractQueryParams = (inputString: string) => {
  const queryParams: KeyValuePair[] = [];

  inputString = split(inputString, "?")[1];

  if (inputString) {
    const queryParamsList = split(inputString, "&");
    forEach(queryParamsList, (queryParam) => {
      const queryParamValues = split(queryParam, "=");
      queryParams.push({
        id: Math.random(),
        key: queryParamValues[0] ?? "",
        value: queryParamValues[1] ?? "",
        isEnabled: true,
      });
    });
  }

  return queryParams;
};

export const queryParamsToURLString = (queryParams: KeyValuePair[], inputString: string) => {
  const baseUrl = split(inputString, "?")[0];
  const enabledParams = queryParams.filter((param) => param.isEnabled ?? true);

  const queryString = enabledParams
    .map(({ key, value }) => {
      if (value === undefined || value === "") {
        return key;
      } else {
        return `${key}=${value}`;
      }
    })
    .filter(Boolean)
    .join("&");

  return `${baseUrl}${queryString ? `?${queryString}` : queryString}`;
};

export const filterRecordsBySearch = (
  records: RQAPI.ApiClientRecord[],
  searchValue: string
): RQAPI.ApiClientRecord[] => {
  if (!searchValue) return records;

  const search = searchValue.toLowerCase();
  const matchingRecords = new Set<string>();

  const childrenMap = new Map<string, Set<string>>();
  const parentMap = new Map<string, string>();

  records.forEach((record) => {
    if (record.collectionId) {
      parentMap.set(record.id, record.collectionId);
      if (!childrenMap.has(record.collectionId)) {
        childrenMap.set(record.collectionId, new Set());
      }
      childrenMap.get(record.collectionId).add(record.id);
    }
  });

  // Add all children records of a collection
  const addChildrenRecords = (collectionId: string) => {
    const children = childrenMap.get(collectionId) || new Set();
    children.forEach((childId) => {
      matchingRecords.add(childId);
      if (childrenMap.has(childId)) {
        addChildrenRecords(childId);
      }
    });
  };

  // Add all parent collections of a record
  const addParentCollections = (recordId: string) => {
    const parentId = parentMap.get(recordId);
    if (parentId) {
      matchingRecords.add(parentId);
      addParentCollections(parentId);
    }
  };

  // First pass: direct matches and their children
  records.forEach((record) => {
    if (record.name.toLowerCase().includes(search)) {
      matchingRecords.add(record.id);

      // If collection matches, add all children records
      if (isApiCollection(record)) {
        addChildrenRecords(record.id);
      }
    }
  });

  // Second pass: add parent collections
  matchingRecords.forEach((id) => {
    addParentCollections(id);
  });

  return records.filter((record) => matchingRecords.has(record.id));
};

export const clearExpandedRecordIdsFromSession = (keysToBeDeleted: string[]) => {
  const activeKeys = sessionStorage.getItem(SESSION_STORAGE_EXPANDED_RECORD_IDS_KEY, []);

  if (keysToBeDeleted.length === 0) {
    return;
  }

  const updatedActiveKeys: string[] = [];

  activeKeys.forEach((key: string) => {
    if (!keysToBeDeleted.includes(key)) updatedActiveKeys.push(key);
  });

  sessionStorage.setItem(SESSION_STORAGE_EXPANDED_RECORD_IDS_KEY, updatedActiveKeys);
};

const getParentIds = (data: RQAPI.ApiClientRecord[], targetId: RQAPI.ApiClientRecord["id"]) => {
  const idToCollectionMap = data.reduce(
    (collectionIdMap: Record<RQAPI.ApiClientRecord["id"], RQAPI.ApiClientRecord["id"]>, item) => {
      collectionIdMap[item.id] = item.collectionId || "";
      return collectionIdMap;
    },
    {}
  );

  const parentIds = [];

  let currentId = idToCollectionMap[targetId];
  while (currentId) {
    parentIds.push(currentId);
    currentId = idToCollectionMap[currentId];
  }

  return parentIds;
};

export const getRecordIdsToBeExpanded = (
  id: RQAPI.ApiClientRecord["id"],
  expandedKeys: RQAPI.ApiClientRecord["id"][],
  records: RQAPI.ApiClientRecord[]
) => {
  // If the provided ID is null or undefined, return the existing active keys.
  if (!id) {
    return expandedKeys;
  }

  const expandedKeysCopy = [...expandedKeys];

  const parentIds = getParentIds(records, id);

  // Include the original ID itself as an active key.
  parentIds.push(id);

  parentIds.forEach((parent) => {
    if (!expandedKeysCopy.includes(parent)) {
      expandedKeysCopy.push(parent);
    }
  });

  return expandedKeysCopy;
};

export const apiRequestToHarRequestAdapter = (apiRequest: RQAPI.HttpRequest): HarRequest => {
  const harRequest: HarRequest = {
    method: apiRequest.method,
    url: apiRequest.url,
    headers: apiRequest.headers.map(({ key, value }) => ({ name: key, value })),
    queryString: apiRequest.queryParams.map(({ key, value }) => ({ name: key, value })),
    httpVersion: "HTTP/1.1",
    cookies: [],
    bodySize: -1,
    headersSize: -1,
  };

  if (supportsRequestBody(apiRequest.method)) {
    if (apiRequest?.contentType === RequestContentType.RAW) {
      harRequest.postData = {
        mimeType: RequestContentType.RAW,
        text: apiRequest.body as string,
      };
    } else if (apiRequest?.contentType === RequestContentType.JSON) {
      harRequest.postData = {
        mimeType: RequestContentType.JSON,
        text: apiRequest.body as string,
      };
    } else if (apiRequest?.contentType === RequestContentType.FORM) {
      harRequest.postData = {
        mimeType: RequestContentType.FORM,
        params: (apiRequest.body as KeyValuePair[]).map(({ key, value }) => ({ name: key, value })),
      };
    }
  }

  return harRequest;
};

export const filterOutChildrenRecords = (
  selectedRecords: Set<RQAPI.ApiClientRecord["id"]>,
  childParentMap: Map<RQAPI.ApiClientRecord["id"], RQAPI.ApiClientRecord["id"]>,
  recordsMap: Record<RQAPI.ApiClientRecord["id"], RQAPI.ApiClientRecord>
) =>
  [...selectedRecords]
    .filter((id) => !childParentMap.get(id) || !selectedRecords.has(childParentMap.get(id)))
    .map((id) => recordsMap[id]);

export const processRecordsForDuplication = (
  recordsToProcess: RQAPI.ApiClientRecord[],
  apiClientRecordsRepository: ApiClientRecordsInterface<Record<string, any>>
) => {
  const recordsToDuplicate: RQAPI.ApiClientRecord[] = [];
  const queue: RQAPI.ApiClientRecord[] = [...recordsToProcess];

  while (queue.length > 0) {
    const record = queue.shift()!;

    if (record.type === RQAPI.RecordType.COLLECTION) {
      const newId = apiClientRecordsRepository.generateCollectionId(`(Copy) ${record.name}`, record.collectionId);

      const collectionToDuplicate: RQAPI.CollectionRecord = Object.assign({}, record, {
        id: newId,
        name: `(Copy) ${record.name}`,
        data: omit(record.data, "children"),
      });

      recordsToDuplicate.push(collectionToDuplicate);

      if (record.data.children?.length) {
        const childrenToDuplicate = record.data.children.map((child) =>
          Object.assign({}, child, { collectionId: newId })
        );
        queue.push(...childrenToDuplicate);
      }
    } else {
      const requestToDuplicate: RQAPI.ApiClientRecord = Object.assign({}, record, {
        id: apiClientRecordsRepository.generateApiRecordId(record.collectionId),
        name: `(Copy) ${record.name}`,
      });

      recordsToDuplicate.push(requestToDuplicate);
    }
  }

  return recordsToDuplicate;
};

export const resolveAuth = (
  auth: RQAPI.Auth,
  childDetails: { id: string; parentId: string },
  getParentChain: (id: string) => string[],
  getData: (id: string) => RQAPI.ApiClientRecord
): RQAPI.Auth => {
  //create a record array
  const apiRecords: RQAPI.ApiClientRecord[] = [];
  const parentChainIds = getParentChain(childDetails?.id);
  for (const parentId in parentChainIds) {
    const parentRecord = getData(parentChainIds[parentId]);
    if (parentRecord) {
      apiRecords.push(parentRecord);
    }
  }

  if (auth?.currentAuthType === Authorization.Type.INHERIT) {
    auth = inheritAuthFromParent(childDetails, apiRecords);
  }
  return auth;
};

export const parseHttpRequestEntry = (
  entry: RQAPI.HttpApiEntry,
  childDetails: { id: string; parentId: string },
  helpers: {
    getParentChain: (id: string) => string[];
    getData: (id: string) => RQAPI.ApiClientRecord;
    resolver: <U extends Record<string, any>>(input: U) => U;
  }
): AutogeneratedFieldsStore["namespaces"] => {
  const { getParentChain, getData, resolver } = helpers;
  const result: AutogeneratedFieldsStore["namespaces"] = {};
  if (!entry) {
    return result;
  }
  const { auth } = entry;
  const currentAuth = resolveAuth(auth, childDetails, getParentChain, getData);
  const authNamespaceContents = parseAuth(currentAuth, resolver);
  result.auth = authNamespaceContents;

  const contentType = entry.request.contentType;
  if (contentType) {
    result.content_type = parseContentType(entry.request.contentType);
  }
  return result;
};

export const getRequestTypeForAnalyticEvent = (
  isExample: RQAPI.ApiRecord["isExample"],
  url: RQAPI.ApiRecord["data"]["request"]["url"]
): string => {
  if (isExample) {
    return "example_collection";
  }

  const echoEndpoint = "https://app.requestly.io/echo";
  if (url === echoEndpoint) {
    return "echo";
  }

  return "custom";
};

export function isHttpApiRecord(record: RQAPI.ApiRecord): record is RQAPI.HttpApiRecord {
  if (record.data.type) {
    return record.data.type === RQAPI.ApiEntryType.HTTP;
  }

  // fallback for older records where type field was not present
  return true;
}

export function isGraphQLApiRecord(record: RQAPI.ApiRecord): record is RQAPI.GraphQLApiRecord {
  return record.data.type === RQAPI.ApiEntryType.GRAPHQL;
}

export const isGraphQLApiEntry = (entry: RQAPI.ApiEntry): entry is RQAPI.GraphQLApiEntry => {
  return entry.type === RQAPI.ApiEntryType.GRAPHQL;
};

export const isHTTPApiEntry = (entry: RQAPI.ApiEntry): entry is RQAPI.HttpApiEntry => {
  if (entry.type) {
    return entry.type === RQAPI.ApiEntryType.HTTP;
  }

  // fallback for older records where type field was not present
  return true;
};

export const isHttpResponse = (response: RQAPI.Response): response is RQAPI.HttpResponse => {
  return "redirectUrl" in response;
};

export const isGraphQLResponse = (response: RQAPI.Response): response is RQAPI.GraphQLResponse => {
  return "type" in response;
};

export const getFileExtension = (fileName: string) => {
  const extension = fileName.includes(".") ? fileName.slice(fileName.lastIndexOf(".")) : "";
  return extension;
};

export const formatBytes = (bytes: number) => {
  if (bytes === 0) return "0 B";
  const k = 1024;
  const sizes = ["B", "KB", "MB", "GB", "TB"];
  const i = Math.floor(Math.log(bytes) / Math.log(k));

  return `${parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`;
};

export const truncateString = (str: string, maxLength: number) => {
  const nameWithoutExtension = str.includes(".") ? str.slice(0, str.lastIndexOf(".")) : str;
  if (nameWithoutExtension.length > maxLength) {
    return nameWithoutExtension.slice(0, maxLength) + "...";
  } else {
    return nameWithoutExtension;
  }
};

export const checkForLargeFiles = (body: RQAPI.RequestBody): boolean => {
  if (Array.isArray(body)) {
    return body.some((item: any) => {
      if (item.type === "file" && item.value && Array.isArray(item.value)) {
        return item.value.some((file: any) => {
          return file.size && file.size > LARGE_FILE_SIZE;
        });
      }
      return false;
    });
  }

  return false;
};

export const extractPathVariablesFromUrl = (url: string) => {
  if (!url) {
    return [];
  }

  const urlWithScheme = addUrlSchemeIfMissing(url);
  let pathname: string = "";
  try {
    pathname = new URL(urlWithScheme).pathname;
  } catch (error) {
    Logger.log("Invalid URL while extracting path variables:", error);
    return [];
  }

  // Allow all characters except URL reserved characters: : / ? # [ ] @ ! $ & ' ( ) * + , ; =
  // Also exclude whitespace and control characters for practical reasons
  const variablePattern = /(?<!:):([^:/?#[\]@!$&'()*+,;=\s]+)/g;
  const variables: string[] = [];
  let match;

  while ((match = variablePattern.exec(pathname)) !== null) {
    const variableName = match[1];
    if (!variables.includes(variableName)) {
      variables.push(variableName);
    }
  }

  return variables;
};

export const createBodyContainer = (params: {
  contentType: RequestContentType;
  body: RQAPI.RequestBody;
}): RQAPI.RequestBodyContainer => {
  const { contentType, body } = params;
  if (body === null || body === undefined) {
    return {};
  }

  switch (contentType) {
    case RequestContentType.FORM:
      return {
        form: body as RQAPI.RequestFormBody,
      };
    case RequestContentType.MULTIPART_FORM:
      return {
        multipartForm: body as RQAPI.MultipartFormBody,
      };
    case RequestContentType.JSON:
      return {
        text: body as RQAPI.RequestJsonBody,
      };
    case RequestContentType.RAW:
      return {
        text: body as RQAPI.RequestRawBody,
      };
    case RequestContentType.HTML:
      return {
        text: body as RQAPI.RequestHtmlBody,
      };
    case RequestContentType.JAVASCRIPT:
      return {
        text: body as RQAPI.RequestJavascriptBody,
      };
    case RequestContentType.XML:
      return {
        text: body as RQAPI.RequestXmlBody,
      };
    default:
      return {
        text: body as RQAPI.RequestRawBody,
      };
  }
};

type ParsedResult = {
  data: Record<string, any>[];
  count: number;
};

export const parseJsonText = async (content: string, ajvSchema?: SchemaObject): Promise<ParsedResult> => {
  try {
    const jsonData = await utilityWorker.parseJsonText(content);

    if (!Array.isArray(jsonData)) {
      Logger.log("[parseJsonText] JSON is not an array");
      throw new Error("JSON is not an array");
    }

    if (ajvSchema) {
      const ajv = new Ajv();
      const validateData = ajv.compile(ajvSchema);

      if (!validateData(jsonData)) {
        Logger.log("[parseJsonText] JSON validation failed:", validateData.errors);
        throw new Error("JSON validation failed");
      }
    }

    return { data: jsonData, count: jsonData.length };
  } catch (e) {
    Logger.log("[parseJsonText] Failed to parse JSON:", e);
    throw new Error("Failed to parse JSON");
  }
};

export const parseCsvText = async (content: string): Promise<ParsedResult> => {
  return new Promise((resolve, reject) => {
    Papa.parse(content, {
      header: true,
      skipEmptyLines: true,
      dynamicTyping: true,
      complete: (results) => {
        const isSingleColumn = results.meta.fields && results.meta.fields.length === 1;

        // Refer: https://github.com/mholt/PapaParse/issues/165
        // Filter out UndetectableDelimiter error only for single-column CSVs
        const criticalErrors = results.errors.filter((error) => {
          if (error.code === "UndetectableDelimiter" && isSingleColumn) {
            return false; // Ignore this error for single-column CSVs
          } else {
            return true; // Keep all other errors
          }
        });

        if (criticalErrors.length) {
          Logger.log("[parseCsvText] Failed to parse CSV:", criticalErrors);
          reject(new Error("Failed to parse CSV"));
        } else {
          const data = results.data as Record<string, any>[];

          resolve({
            data,
            count: data.length,
          });
        }
      },
      error: (error: any) => {
        Logger.log("[parseCsvText] Failed to parse CSV:", error);
        reject(error);
      },
    });
  });
};

const CollectionRunnerAjvSchema: SchemaObject = {
  type: "array",
  items: {
    type: "object",
    additionalProperties: {
      type: ["string", "number", "boolean", "null"],
    },
  },
};
export const parseCollectionRunnerDataFile = async (filePath: string, maxlimit?: number) => {
  if (!filePath) {
    throw new NativeError("File path is empty!");
  }

  const fileExtension = getFileExtension(filePath).toLowerCase();
  const fileContents = await getFileContents(filePath);

  switch (fileExtension) {
    case ".csv": {
      const parsedData = await parseCsvText(fileContents);
      if (maxlimit && parsedData.count > maxlimit) {
        trackCollectionRunnerRecordLimitExceeded({ record_count: parsedData.count });
        parsedData.data = parsedData.data.slice(0, maxlimit);
      }
      return parsedData;
    }
    case ".json": {
      const parsedData = await parseJsonText(fileContents, CollectionRunnerAjvSchema);
      if (maxlimit && parsedData.count > maxlimit) {
        trackCollectionRunnerRecordLimitExceeded({ record_count: parsedData.count });
        parsedData.data = parsedData.data.slice(0, maxlimit);
      }
      return parsedData;
    }
    default: {
      throw new NativeError("Unsupported data file format!").addContext({ fileExtension });
    }
  }
};
